use std::{future::Future, pin::Pin, sync::Arc};

use cookie::Cookie;

use ntex::{
    http::HttpMessage,
    web::{ErrorRenderer, FromRequest},
};

use reqwest::Client;
use serde::{Deserialize, Serialize};

use crate::{errors::CustomError, AppState};

// frontend github auth pass in code
#[derive(Debug, Clone, Deserialize)]
pub struct Login {
    pub code: String,
}

// github return access_token
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct GithubUserInfo {
    // github user id
    pub id: u32,
    // login user name (not nickname)
    pub login: String,
    // user head photo address
    pub avatar_url: String,
}

// the user info that return to client
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserInfo {
    // github user id
    pub id: u32,
    // github user name(not nickname)
    pub login: String,
    // user head photo address
    pub avatar_url: String,
    // check if is admin
    pub is_admin: bool,
}

// website all user (include admin), for TODO:
#[derive(Debug, Clone)]
pub struct User {
    pub id: u32,
}

// website administrator, for TODO:
#[derive(Debug, Clone)]
pub struct Admin {
    pub id: u32,
}

impl<E: ErrorRenderer> FromRequest<E> for User {
    type Error = CustomError;
    type Future = Pin<Box<dyn Future<Output = Result<Self, Self::Error>>>>;

    fn from_request(req: &ntex::web::HttpRequest, _: &mut ntex::http::Payload) -> Self::Future {
        let db_pool = Arc::clone(req.app_state::<Arc<AppState>>().unwrap())
            .db_pool
            .clone();

        let access_token = req.cookie("ACCESS_TOKEN");

        let fut = async move {
            let access_token = match access_token {
                Some(c) => c,
                None => return Err(CustomError::AuthFailed("you have not login yet".into())),
            };

            let user_id = match get_user_id(&access_token).await {
                Ok(id) => id,
                Err(e) => {
                    return Err(e);
                }
            };

            if sqlx::query!("SELECT id FROM users WHERE id = $1", user_id as i32)
                .fetch_optional(&db_pool)
                .await?
                .is_none()
            {
                // not found
                return Err(CustomError::AuthFailed(
                    "you have not login in github, please login github".into(),
                ));
            }

            Ok(Self { id: user_id })
        };

        Box::pin(fut)
    }
}

impl<E: ErrorRenderer> FromRequest<E> for Admin {
    type Error = CustomError;
    type Future = Pin<Box<dyn Future<Output = Result<Self, Self::Error>>>>;

    fn from_request(req: &ntex::web::HttpRequest, _: &mut ntex::http::Payload) -> Self::Future {
        let db_pool = Arc::clone(req.app_state::<Arc<AppState>>().unwrap())
            .db_pool
            .clone();

        // the access token in cookies
        let access_token = req.cookie("ACCESS_TOKEN");

        let fut = async move {
            let access_token = match access_token {
                Some(c) => c,
                None => return Err(CustomError::AuthFailed("you have not login yet".into())),
            };

            let user_id = match get_user_id(&access_token).await {
                Ok(id) => id,
                Err(e) => {
                    return Err(e);
                }
            };

            if sqlx::query!("SELETE id FROM users WHERE id = $1", user_id as i32)
                .fetch_optional(&db_pool)
                .await?
                .is_some()
            {
                // found it
                if user_id != 6969 {
                    return Err(CustomError::AuthFailed(
                        "you are not admin, failed to continue".into(),
                    ));
                }
            } else {
                // not found
                return Err(CustomError::AuthFailed(
                    "you have never login with github account, please login with github account"
                        .into(),
                ));
            }

            Ok(Self { id: user_id })
        };

        Box::pin(fut)
    }
}

async fn get_user_id(access_token: &Cookie<'_>) -> Result<u32, CustomError> {
    let client = Client::new();

    let user_info = client
        .get("https://api.github.com/user") // hard code
        .bearer_auth(access_token.value())
        // github api force us set User Agent,
        // see: https://docs.github.com/en/rest/using-the-rest-api/getting-started-with-the-rest-api?apiVersion=2022-11-28
        .header("User-Agent", "xxxvideos") // set xxxvideos for fun, it can be any string
        .send()
        .await;

    let user_id = match user_info {
        Ok(r) => {
            match r.json::<GithubUserInfo>().await {
                Ok(i) => i.id,
                Err(_) => {
                    // unable to parse, maybe github return some error message
                    return Err(CustomError::BadRequest(
            "unable to get github user info, maybe you have provided incorrect access token, please login again"
            .into(),
          ));
                }
            }
        }

        Err(_) => {
            return Err(CustomError::InternalServerError(
                "unable to get github user info, please retry".into(),
            ))
        }
    };

    Ok(user_id)
}
